/*
 * Copyright (c) 2025 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import {
    Stmt,
    ArkMethod,
    ArkReturnStmt, ClassType, UnionType, ts, AstTreeUtils
} from "arkanalyzer";
import Logger, {LOG_MODULE_TYPE} from 'arkanalyzer/lib/utils/logger';
import {ClassMatcher, MatcherCallback, MatcherTypes, MethodMatcher} from "../../matcher/Matchers";
import {Defects, IssueReport} from "../../model/Defects";
import {Rule} from "../../model/Rule";
import {BaseChecker, BaseMetaData} from "../../checker/BaseChecker";
import {RuleListUtil} from "../../utils/common/DefectsList";

const logger = Logger.getLogger(LOG_MODULE_TYPE.HOMECHECK, 'NoUnsafeReturnCheck');
const gMetaData: BaseMetaData = {
    severity: 1,
    ruleDocPath: "docs/no-unsafe-return-check.md",
    description: "Unsafe return of an `any` typed value."
};

export class NoUnsafeReturnCheck implements BaseChecker {
    readonly metaData: BaseMetaData = gMetaData;
    public rule: Rule;
    public defects: Defects[] = [];
    public issues: IssueReport[] = [];

    private clsMatcher: ClassMatcher = {
        matcherType: MatcherTypes.CLASS
    };

    private buildMatcher: MethodMatcher = {
        matcherType: MatcherTypes.METHOD,
        class: [this.clsMatcher],
    };

    public registerMatchers(): MatcherCallback[] {
        const matchBuildTs: MatcherCallback = {
            matcher: this.buildMatcher,
            callback: this.check
        }
        return [matchBuildTs];
    }

    public check = (targetMethod: ArkMethod): void => {
        const stmts = targetMethod.getBody()?.getCfg().getStmts() ?? [];
        if (!this.getFileExtension(targetMethod.getDeclaringArkClass().getDeclaringArkFile().getName(), 'ts')) {
            return;
        }
        for (const stmt of stmts) {
            if (stmt instanceof ArkReturnStmt) {
                if (this.checkReturnStatementBlock(stmt, targetMethod)) {
                    return;
                }
            }
        }
    }

    private checkReturnStatementBlock(stmt: ArkReturnStmt, targetMethod: ArkMethod): boolean {
        if (stmt.getOp()) {
            const type = stmt.getOp().getType();
            if (type instanceof ClassType && !this.isAllowedClassType(type)) {
                return true;
            }
            if (this.isUnionType(type)) {
                return true;
            }
        }
        if (this.analyzeReturnStmt(stmt)) {
            return true;
        }
        this.checkReturnType(stmt, targetMethod.getCode() ?? '');
        return false;
    }

    private isAllowedClassType(type: ClassType): boolean {
        const className = type.getClassSignature().getClassName();
        return className === 'Map' || className === 'Set';
    }

    private isUnionType(type: any): boolean {
        return type instanceof UnionType;
    }

    private checkReturnStatement(node: ts.Node, foundAnyKeyword: { value: boolean }): void {
        if (!ts.isReturnStatement(node)) {
            return;
        }
        const expr = node.expression;
        if (!expr || !ts.isAsExpression(expr)) {
            return;
        }
        const typeRef = expr.type;
        if (!ts.isTypeReferenceNode(typeRef)) {
            return;
        }
        if (typeRef.typeArguments?.some(typeArg =>
            ts.isArrayTypeNode(typeArg) && typeArg.elementType.kind === ts.SyntaxKind.AnyKeyword
        )) {
            return;
        }
        if (typeRef.typeArguments?.some(typeArg => typeArg.kind !== ts.SyntaxKind.AnyKeyword)) {
            foundAnyKeyword.value = true;
        }
    }

    private checkExpressionStatement(node: ts.Node, foundAnyKeyword: { value: boolean }): void {
        if (ts.isExpressionStatement(node)) {
            if (ts.isArrayLiteralExpression(node.expression)) {
                if (node.expression.elements.length === 0) {
                    foundAnyKeyword.value = true;
                }
            }
        }
    }

    private analyzeReturnStmt(stmt: ArkReturnStmt): boolean {
        const code = stmt.getOriginalText() || '';
        const asRoot = AstTreeUtils.getASTNode('example.ts', code);
        const foundAnyKeyword = {value: false};
        ts.forEachChild(asRoot, node => {
            this.checkReturnStatement(node, foundAnyKeyword);
            this.checkExpressionStatement(node, foundAnyKeyword);
        });
        return foundAnyKeyword.value;
    }

    private checkReturnType(stmt: ArkReturnStmt, methodCode: string): void {
        if (stmt.getOp().getType().getTypeString().includes('any')) {
            if (!this.checkUnknownBeforeBrace(methodCode)) {
                this.addIssueReport(stmt);
            }
        }
    }

    private checkUnknownBeforeBrace(code: string): boolean {
        const regex = /unknown\s*\{/;
        return regex.test(code);
    }

    private getFileExtension(filePath: string, filetype: string): boolean {
        const match = filePath.match(/\.([0-9a-zA-Z]+)$/);
        if (match) {
            const extension = match[1];
            return extension === filetype;
        }
        return false;
    }

    private addIssueReport(stmt: Stmt): void {
        const severity = this.rule.alert ?? this.metaData.severity;
        const warnInfo = this.getLineAndColumn(stmt);
        const defect = new Defects(warnInfo.line!, warnInfo.startCol, warnInfo.endCol, this.metaData.description, severity,
            this.rule.ruleId, warnInfo.filePath, this.metaData.ruleDocPath, true, false, false)
        this.issues.push(new IssueReport(defect, undefined));
        RuleListUtil.push(defect);
    }

    private getLineAndColumn(stmt: Stmt): { line: number, startCol: number, endCol: number, filePath: string } {
        const originalPositions = stmt.getOriginPositionInfo();
        if (originalPositions) {
            const line = originalPositions.getLineNo();
            const arkFile = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile();
            if (arkFile) {
                let startCol = originalPositions.getColNo();
                const endCol = originalPositions.getColNo();
                const filePath = stmt.getCfg()?.getDeclaringMethod().getDeclaringArkFile().getFilePath();
                return {line, startCol, endCol, filePath: filePath}
            } else {
                logger.debug('originStmt or arkFile is null');
            }
        }
        return {line: -1, startCol: -1, endCol: -1, filePath: ''};
    }
}